// @vitest-environment node import { beforeEach, describe, expect, it, vi } from "vitest"; const { authenticateRequest, getAttachmentObject, getGateFileContent, getOwnedAttachment } = vi.hoisted(() => ({ authenticateRequest: vi.fn(), getAttachmentObject: vi.fn(), getGateFileContent: vi.fn(), getOwnedAttachment: vi.fn(), })); vi.mock("server-only", () => ({})); vi.mock("@/lib/db/pool", () => ({ getPool: () => ({}) })); vi.mock("@/lib/auth/server", async (importOriginal) => ({ ...(await importOriginal()), authenticateRequest, })); vi.mock("@/lib/db/attachments", () => ({ getOwnedAttachment })); vi.mock("@/lib/chat/gate-files", () => ({ getGateFileContent })); vi.mock("@/lib/storage/s3", () => ({ getAttachmentObject })); import { GET } from "@/app/api/attachments/[id]/route"; import { AuthError } from "@/lib/auth/server"; const attachment = { id: "00000000-0000-4000-8000-000000000077", name: "договор 1.txt", contentType: "text/plain", size: 15, gateFileId: "file-opaque_123", objectKey: null, }; beforeEach(() => { authenticateRequest.mockReset().mockResolvedValue({ id: "00000000-0000-4000-8000-000000000001", email: "alice@example.com", }); getOwnedAttachment.mockReset().mockResolvedValue(attachment); getGateFileContent.mockReset().mockResolvedValue(streamOf("private contents")); getAttachmentObject.mockReset().mockResolvedValue(streamOf("legacy contents")); }); describe("GET /api/attachments/:id", () => { it("streams Gate content through the authenticated same-origin route", async () => { const request = new Request(`http://localhost/api/attachments/${attachment.id}`, { headers: { Authorization: "Bearer exact-user-jwt" }, }); const response = await GET(request, { params: Promise.resolve({ id: attachment.id }) }); expect(response.status).toBe(200); expect(response.headers.get("Location")).toBeNull(); expect(response.headers.get("Cache-Control")).toBe("no-store"); expect(response.headers.get("Content-Type")).toBe("text/plain"); expect(response.headers.get("Content-Length")).toBe("15"); expect(response.headers.get("Content-Disposition")).toBe( "attachment; filename*=UTF-8''%D0%B4%D0%BE%D0%B3%D0%BE%D0%B2%D0%BE%D1%80%201.txt", ); expect(await response.text()).toBe("private contents"); expect(getGateFileContent).toHaveBeenCalledWith( "file-opaque_123", "Bearer exact-user-jwt", expect.any(AbortSignal), ); expect(getAttachmentObject).not.toHaveBeenCalled(); }); it("keeps legacy object-key rows downloadable during migration", async () => { getOwnedAttachment.mockResolvedValue({ ...attachment, gateFileId: null, objectKey: "legacy/private-key", }); const response = await GET( new Request(`http://localhost/api/attachments/${attachment.id}`, { headers: { Authorization: "Bearer exact-user-jwt" }, }), { params: Promise.resolve({ id: attachment.id }) }, ); expect(await response.text()).toBe("legacy contents"); expect(getAttachmentObject).toHaveBeenCalledWith("legacy/private-key"); expect(getGateFileContent).not.toHaveBeenCalled(); }); it("does not resolve content when the caller is unauthorized", async () => { authenticateRequest.mockRejectedValue(new AuthError()); const response = await GET(new Request("http://localhost/api/attachments/id"), { params: Promise.resolve({ id: "id" }), }); expect(response.status).toBe(401); expect(getOwnedAttachment).not.toHaveBeenCalled(); expect(getGateFileContent).not.toHaveBeenCalled(); expect(getAttachmentObject).not.toHaveBeenCalled(); }); it("does not resolve content for another user's attachment", async () => { getOwnedAttachment.mockResolvedValue(null); const response = await GET( new Request("http://localhost/api/attachments/id", { headers: { Authorization: "Bearer exact-user-jwt" }, }), { params: Promise.resolve({ id: "id" }) }, ); expect(response.status).toBe(404); expect(getGateFileContent).not.toHaveBeenCalled(); expect(getAttachmentObject).not.toHaveBeenCalled(); }); }); function streamOf(content: string): ReadableStream { return new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode(content)); controller.close(); }, }); }